package dungeon;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import basicPackage.Coordinate;
import be.kuleuven.cs.som.annotate.Raw;


/**
 * A class representing a composite dungeon containing other dungeons on their respective coordinate.
 * 
 * @invar	| getMaxCoordinate != null
 * @invar	| getMaxCoordinate().isPositive
 * @invar	| if(getSuperCompositedungeon != null)
 * 			|	getMaxCoordinate().add(getDungeonCoordinate()).isSmallerOrEqualTo(getSuperCompositeDungeon().geMaxCoordinate
 * @invar	| getSuperCompositeDungeon() != this
 * @invar	| !containDungeon(getSupercompositeDungeon())
 * @invar	| (getDungeonCoordinate() == null || getDungeonCoordinate().isPositive())
 * @invar	| if(containsElementCoordinate(coordinate)
 * 			|	coordinate.isPositive && coordinate.isSmallerOrEqualTo(getMaxCoordinate()) && !coordinate.hasSameValueForAllCoordinates()
 * @invar	| if(containsSquareCoordinate(coordinate)
 * 			|	coordinate.isPositive && coordinate.isSmallerOrEqualTo(getMaxCoordinate()) && !coordinate.hasSameValueForAllCoordinates()
 * @invar	| if( exists coordinate: getElement(coordinate) != null)
 * 			|	exists no coordinate2: coordinate2 != coordinate && getElement(coordinate) == getElement(coordinate2)
 * @invar	| if( exists element: exists coordinate: getElement(coordinate) == element))
 * 			|	exists no element2: element2 != element: getElement(coordinate) == element2
 * @invar	| if( exists coordinate: getSquare(coordinate) != null)
 * 			|	exists no coordinate2: coordinate2 != coordinate && getSquare(coordinate) == getSquare(coordinate2)
 * @invar	| if( exists square: exists coordinate: getSquare(coordinate) == square))
 * 			|	exists no square2: square != square2: getSquare(coordinate) == square2
 * @invar	| if(containsSquareCoordinate(coordinate))
 * 			|	for each Direction.i: containsSquareCoordinate(coordinate.getNeighboringCoordinates().get(i))
 * 			|		getSquare(coordinate).getBorderingSquare(i) == getSquare(coordinate.getNeighboringCoordinates().get(i))
 * 			|		getSquare(coordinate.getNeighboringCoordinates().get(i)).getBorderingSquare(i.oppositeDirection()) == getSquare(coordinate)
 * 
 * @author Laurens Vanderstraeten
 *
 */
public class CompositeDungeon<D extends Dungeon<?>> extends Dungeon<D>{
	
	/**
	 * Construct a composite dungeon.
	 * 
	 * @param 	maxSquareCoordinate
	 * 			The maximum coordinate determining the outer point in the cuboid space of 
	 * 			possible square coordinates.
	 * @post	| getMaxCoordinate() == maxCoordinate
	 * @throws	IllegalArgumentException
	 * 			| !isValidMaxCoordinate(maxCoordinate)
	 * 
	 */
	@Raw
	public CompositeDungeon(Coordinate maxSquareCoordinate) throws IllegalArgumentException {
		super(maxSquareCoordinate);
	}
	
	/** 
	 * Check whether the given composite dungeon is a valid super composite dungeon.
	 * 
	 * @param	superCompositeDungeon
	 * 			| The dungeon to be set as superCompositeDungeon
	 * @return	| if(!super.isValidSuperCompositeDungeon(superCompositeDungeon))
	 *			|	result == false
	 *			| if(containsDungeon(superCompositeDungeon))
	 *			|	result == false
	 *			| else then 
	 *			|	result == true
	 */
	@Override @Raw
	public boolean isValidSuperCompositeDungeon(CompositeDungeon<?> superCompositeDungeon) {
		if(!super.isValidSuperCompositeDungeon(superCompositeDungeon))
			return false;
		if(containsDungeon(superCompositeDungeon))
			return false;
		return true;
	}
	
	
	/**
	 * Return the square on the given coordinate.
	 * 
	 * @param	coordinate
	 * 			The coordinate of the square to be retrieved.
	 * @return	| if(exists i: exist d element getElementMap().values(): d.getSquare(coordinate.substract(d.getDungeonCoordinate())) != null
	 * 			|	result == d.getSquare(coordinate.substract(d.getDungeonCoordinate()))
	 * 			| else then
	 * 			|	result == null
	 */
	@Override
	public Square getSquare(Coordinate coordinate) {
		Iterator<D> elementIterator = getElementIterator();
		while(elementIterator.hasNext()){
			D dungeon = elementIterator.next();
			if(coordinate.isLargerOrEqualTo(dungeon.getDungeonCoordinate()) && coordinate.isSmallerOrEqualTo(dungeon.getDungeonCoordinate().add(dungeon.getMaxCoordinate()))){
				Square result = dungeon.getSquare(coordinate.subtract(dungeon.getDungeonCoordinate()));
				if(result != null)
					return result;
			}
		}
		return null;
	}
	
	/**
	 * Return the dungeon on the given coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate of the dungeon to be retrieved.
	 * @return	| if(exists i: exist d element getElementMap().values(): d.getDungeonCoordinate() == coordinate 
	 * 			|	result == d
	 * 			| if(exists i: exist d element getElementMap().values(): d instanceof CompositeDungeon<?> && d.getDungeon(coordinate.substract(d.getDungeonCoordinate())) != null
	 * 			| 	result == d.getDungeon(coordinate.substract(d.getDungeonCoordinate()))
	 * 			| else then
	 * 			| 	result == null
	 */
	public Dungeon<?> getDungeon(Coordinate coordinate) {
		Iterator<D> elementIterator = getElementIterator();
		while(elementIterator.hasNext()){
			D dungeon = elementIterator.next();
			if(dungeon.getDungeonCoordinate() == coordinate)
				return dungeon;
			if(dungeon instanceof CompositeDungeon<?> && coordinate.isLargerOrEqualTo(dungeon.getDungeonCoordinate()) && coordinate.isSmallerOrEqualTo(dungeon.getDungeonCoordinate().add(dungeon.getMaxCoordinate()))){
				Dungeon<?> result = ((CompositeDungeon<?>)dungeon).getDungeon(coordinate.subtract(dungeon.getDungeonCoordinate()));
				if(result != null)
					return result;
			}
		} 
		return null;
	}
	
	/** 
	 * Return an Iterator iterating all the squares in this dungeon.
	 */
	@Override
	public Iterator<Square> getSquareIterator() {
		return new Iterator<Square>() {
			
			public boolean hasNext() {
				return (nbSquaresLeft != 0);
			}

			public Square next() throws NoSuchElementException {
				if (!hasNext())
					throw new NoSuchElementException();
				if(currentSquareIterator.hasNext()) {
					nbSquaresLeft--;
					return currentSquareIterator.next();
				}
				else {
					currentSquareIterator = dungeonIterator.next().getSquareIterator();
					return next();
				}
			}

			public void remove() throws UnsupportedOperationException {
				throw new UnsupportedOperationException();
			}
		
			private final Iterator<D> dungeonIterator = getElementIterator();
			
			private Iterator<Square> currentSquareIterator;
			
			{
				if(dungeonIterator.hasNext())
					currentSquareIterator = dungeonIterator.next().getSquareIterator();
			}
			
			private int nbSquaresLeft = getNbSquares();

		};
	}
		
	/** 
	 * Return an Iterator iterating all the square coordinates.
	 */
	@Override
	public Iterator<Coordinate> getSquareCoordinateIterator() {
		return new Iterator<Coordinate>() {
			
			public boolean hasNext() {
				return (nbSquaresLeft != 0);
			}

			public Coordinate next() throws NoSuchElementException {
				if (!hasNext())
					throw new NoSuchElementException();
				if(currentCoordinateIterator.hasNext()){
					nbSquaresLeft--;
					return currentCoordinateIterator.next();
				}
				else {
					currentCoordinateIterator = dungeonIterator.next().getSquareCoordinateIterator();
					return next();
				}
			}

			public void remove() throws UnsupportedOperationException {
				throw new UnsupportedOperationException();
			}
		
			private final Iterator<D> dungeonIterator = getElementIterator();
			
			private Iterator<Coordinate> currentCoordinateIterator;
			
			{
				if(dungeonIterator.hasNext())
					currentCoordinateIterator = dungeonIterator.next().getSquareCoordinateIterator();
			}
			
			private int nbSquaresLeft = getNbSquares();

		};
	}
	
	/** 
	 * Return the number of squares within this dungeon.
	 * 
	 * @return	| for all i element getElementMap.values():
	 * 			| 	result = sum(i.getNbSquares)
	 */
	@Override
	public int getNbSquares(){
		int nbSquaresUpTillNow = 0;
		Iterator<D> dungeonIterator = getElementIterator();
		while(dungeonIterator.hasNext()) {
			nbSquaresUpTillNow += dungeonIterator.next().getNbSquares();
		}
		return nbSquaresUpTillNow;
	}
	
	/** 
	 * Check whether this dungeon contains a square on the given coordinate.
	 * 
	 * @param	coordinate
	 * 			The coordinate to be checked.
	 * @return	| if(any i element getElementMap.values(): i.containsSquareCoordinate(coordinate.substract(i.getDungeonCoordinate)))
	 * 			|	result == true
	 * 			| else then
	 * 			|	result == false
	 */
	@Override
	public Boolean containsSquareCoordinate(Coordinate coordinate) {
		Iterator<D> dungeonIterator = getElementIterator();
		while(dungeonIterator.hasNext()){
			D dungeon = dungeonIterator.next();
			if(coordinate.isLargerOrEqualTo(dungeon.getDungeonCoordinate()) && coordinate.isSmallerOrEqualTo(dungeon.getDungeonCoordinate().add(dungeon.getMaxCoordinate())))
				if(dungeon.containsSquareCoordinate(coordinate.subtract(dungeon.getDungeonCoordinate())))
					return true;
		}
		return false;
	}
	
	/**
	 * Check whether this dungeon contains the given dungeon.
	 * 
	 * @param 	dungeon
	 * 			The dungeon to be checked.
	 * @return	| if exists i element getElementMap.values: i == dungeon || i.containsDungeon(dungeon)
	 * 			| 	result == true
	 * 			| else then
	 * 			| 	result == false;
	 */
	public Boolean containsDungeon(Dungeon<?> dungeon) {
		Iterator<D> dungeonIterator = getElementIterator();
		while(dungeonIterator.hasNext()) {
			Dungeon <?> currentDungeon = dungeonIterator.next();
			if(currentDungeon == dungeon)
				return true;
			if(currentDungeon instanceof CompositeDungeon<?>) {
				if(((CompositeDungeon<?>) currentDungeon).containsDungeon(dungeon))
					return true;
			}
		}
		return false;
	}
	
	/**
	 * Check whether the given dungeon is a valid dungeon.
	 * 
	 * @param 	dungeon
	 * 			The dungeon to be checked.
	 * @return	| if(dungeon == null)
	 *			|	result == false
	 *			| if(containsDungeon(dungeon))
	 *			|	result == false
	 *			| else then
	 *			|	result == true
	 */
	public Boolean isValidDungeon(D dungeon) {
		if(dungeon == null)
			return false;
		if(containsDungeon(dungeon))
			return false;
		return true;
	}
	
	/**
	 * Check whether the given dungeon has squares with overlapping square coordinates with this dungeon's root dungeon if it is added on the given relative coordinate in this dungeon.
	 * 
	 * @param 	relativeCoordinateToDungeonOrigin
	 * 			The coordinate of the given dungeon's origin relative towards this dungeon's origin.
	 * @param 	dungeon
	 * 			The dungeon to be checked.
	 * @return	| if(exists coordinate: getRootDungeon().containsSquareCoordinate(coordinate) 
	 * 			|		&& dungeon.containsSquareCoordinate(coordinate.subtract(getWorldCoordinateFromRootDungeon(relativeCoordinateInThisDungeon))))
	 * 			| 	result == true
	 * 			| else then
	 * 			| 	result == false 
	 */
	@Raw
	public boolean hasOverlappingSquareCoordinatesWithRootDungeon(Coordinate relativeCoordinateInThisDungeon, D dungeon) {
		Dungeon<?> rootDungeon = getRootDungeon();
		Coordinate worldCoordinateInThisRootDungeon = getWorldCoordinateInRootDungeon(relativeCoordinateInThisDungeon);
		Iterator<Coordinate> coordinateIterator = dungeon.getSquareCoordinateIterator();
		while(coordinateIterator.hasNext()){
			if(rootDungeon.containsSquareCoordinate(worldCoordinateInThisRootDungeon.add(coordinateIterator.next())))
				return true;
		}
		return false;
	}
	
	/**
	 * Add a the given dungeon to this composite dungeon under the given coordinate.
	 * 
	 * @param 	coordinate
	 * 			The coordinate under which the given dungeon must be registered.
	 * @param 	dungeon
	 * 			The dungeon to be added.
	 * @post	| getElementMap().get(coordinate) == dungeon
	 * @post	| getElement(coordinate) == dungeon
	 * @post	| getDungeon(coordinate) == dungeon
	 * @post	| containsDungeon(dungeon)
	 * @throws 	IllegalArgumentException
	 * 			| !isValidElementCoordinate(coordinate) || !isValidDungeon(dungeon)
	 * 			| !dungeon.getMaxCoordinate().add(coordinate).isSmallerOrEqualTo(getMaxCoordinate())
	 * 			| hasOverlappingSquareCoordinatesWithRootDungeon(coordinate, dungeon)
	 */
	@Raw
	public void addDungeon(Coordinate coordinate, D dungeon) throws IllegalArgumentException {
		if(!isValidElementCoordinate(coordinate) || !isValidDungeon(dungeon))
			throw new IllegalArgumentException();
		if(!dungeon.getMaxCoordinate().add(coordinate).isSmallerOrEqualTo(getMaxCoordinate()))
			throw new IllegalArgumentException();;
		if(hasOverlappingSquareCoordinatesWithRootDungeon(coordinate, dungeon))
			throw new IllegalArgumentException();
		setElement(coordinate, dungeon);
		dungeon.setDungeonCoordinate(coordinate);
		dungeon.setSuperCompositeDungeon((CompositeDungeon<?>) this);
 		connectAllSquares(coordinate);
	}

	
	/**
	 * Connect all squares of the dungeon on the given to it's neighboring squares in the root dungeon.
	 * 
	 * @param 	coordinate
	 * 			The coordinate of the dungeon within this dungeon to have it's square connected.
	 * @post	| for all c: getDungeon(coordinate).containsSquareCoordinate(c)
	 * 			| for all Direction.i:
	 * 			|	if(getRootDungeon().containsSquareCoordinate(getDungeon(coordinate).getWorldCoordinateInRootDungeon(c).getNeighboringCoordinates().get(i)))
	 * 			|		getRootDungeon().getSquare(getDungeon(coordinate).getWorldCoordinateInRootDungeon(c).getNeighboringCoordinates().get(i)).getBorderingSquares().contains(getRootDungeon().getSquare(getDungeon(coordinate).getWorldCoordinateInRootDungeon(c)))
	 * @throws	IllegalArgumentException
	 * 			getDungeon(coordinate) == null
	 */
	public void connectAllSquares(Coordinate coordinate) throws IllegalArgumentException {
		if(getDungeon(coordinate) == null)
			throw new IllegalArgumentException();
		Dungeon<?> rootDungeon = getRootDungeon();
		Iterator<Coordinate> squareCoordinateIterator = getDungeon(coordinate).getSquareCoordinateIterator();
		while(squareCoordinateIterator.hasNext()) {
			Coordinate worldCoordinate = getDungeon(coordinate).getWorldCoordinateInRootDungeon(squareCoordinateIterator.next());
			rootDungeon.resolveConnectionToNeighbours(worldCoordinate);
		}
	}
	
	/**
	 * Add all the square dungeons within this dungeon to the given list.
	 * 
	 * @param	listUpTillNow
	 * 			List to be filled with the square dungeons within this dungeon.
	 * @return	| for each i: containsDungeon(i) && i instance of SquareDungeon<?>
	 * 			| 	listUpTillNow.add(i)
	 * 			| result == new.listUpTillNow
	 */
	protected List<SquareDungeon<?>> getAllLevelsAndShafts(List<SquareDungeon<?>> listUpTillNow) {
		Iterator<D> dungeonIterator = (Iterator<D>) getElementIterator();
		while(dungeonIterator.hasNext()) {
			Dungeon<?> dungeon = dungeonIterator.next();
			if(dungeon instanceof CompositeDungeon<?>)
				((CompositeDungeon<?>) dungeon).getAllLevelsAndShafts(listUpTillNow);
			if(dungeon instanceof SquareDungeon<?>)
				listUpTillNow.add((SquareDungeon<?>) dungeon);
		}
		return listUpTillNow;
	}
	
	/**
	 * Return a List with all the squareDungeons within this dungeon.
	 * 
	 * @return	| for each i: containsDungeon(i) && i instance of SquareDungeon<?>
	 *			| 	list.add(i)
	 * 			| result == list(all i)
	 */
	public List<SquareDungeon<?>> getAllLevelsAndShafts() {
			return getAllLevelsAndShafts(new ArrayList<SquareDungeon<?>>());
	}

}
